/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package coalescent.is.cef.command;

import cef.command.Command;
import cef.command.CommandException;
import cef.command.EvtImpl_Direct;

import coalescent.cef.command.RunRecursionCommand;
import coalescent.cef.command.RunRecursionEventImpl_Direct;
import coalescent.cef.event.RunRecursionEvent;
import coalescent.data.K69Data;
import coalescent.is.Samplers_K69;
import coalescent.is.cef.command.listener.AppNoteIS_OC;
import coalescent.is.cef.event.AppNoteIS_Evt;
import coalescent.is.cef.event.RunSamplers_Evt;
import coalescent.model.K69;
import coalescent.recursion.ProbComputer_Exact;
import coalescent.recursion.RecursionComputers;
import coalescent.statistic.K69_AC;

import commons.core.MutableData;
import commons.core.Simulator;
import commons.is.Sampler;
import commons.util.GUIUtil;
import commons.util.PrintUtil;
import commons.util.ThreadUtil;

import test.coalescent.data.RealDataSets;

import static coalescent.is.cef.command.AppNoteIS_CmdState.Output;

import static commons.util.FormatUtil.format;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.swing.*;

/**
 * Compares the computational efficiency between proposals {@code SD} and {@code HAW} over a
 * number of randomly generated data sets, specified by a model generator and a data simulator.
 *
 * @author Susanta Tewari
 * @version version
 * @history Created on 11/14/13.
 * @since since
 */
public class AppNoteIS_Cmd extends Command<AppNoteIS_Evt> {

    private final String statefulPath = "console/AppNoteIS_Stateful.xml";
    private final boolean overwrite   = true;

    /**
     * Collects the following information from the passed event:
     * <ol>
     * <li>output listeners</li>
     * <li>exe listeners</li>
     * <li>print writer; if none is found sets {@code std out} as the default</li>
     * </ol>
     *
     * @param event event for this command
     */
    public AppNoteIS_Cmd(AppNoteIS_Evt event) {
        super(event);
    }

    @Override
    protected void executeImpl() throws CommandException, InterruptedException {
        realData_RunningTimeAnalysis();
    }

    /**
     * running time comparison for simulated data sets.
     *
     * @throws CommandException
     * @throws InterruptedException
     * @param aSync
     */
    void simData_RunningTimeAnalysis(boolean aSync) throws CommandException, InterruptedException {


        // prepare state
        final AppNoteIS_CmdState state;

        if (!overwrite && isStatePresent()) {    // read state

            try {
                state = AppNoteIS_CmdState.load(statefulPath);
            } catch (Exception e) {
                throw new CommandException("Failed reading state from " + statefulPath, e);
            }

        } else {    // create state

            double[] thetas              = { 1.0, 3.0, 5.0 };
            int[] nVals                  = { 20, 30, 40 };
            final String[] samplerLabels = RunSamplers_EvtImpl_Direct.samplerLabels();

            state = new AppNoteIS_CmdState(thetas, nVals, samplerLabels, 1);

            state.setSamplerCount(3);
            state.setCellSampleSize(3);
            state.setSizePerOrder(1500);
        }


        // start
        final double[] thetas                        = state.getThetas();
        final int[] nVals                            = state.getNVals();
        final Simulator.DATA<K69, K69Data> simulator = getEvent().getDataSimulator();


        // threads
        List<Thread> threads = new ArrayList<>();
        final int jobUnit    = 2;

        for (int i = 0; i < thetas.length; i++) {

            for (int j = 0; j < nVals.length; j++) {

                if (state.containsOutputBySize(i, j, 0)) continue;    // skip if already computed

                int N                               = nVals[j];
                K69 k69                             = new K69(thetas[i]);
                final int cellSampleSize            = state.getCellSampleSize();
                final int sizePerOrder              = state.getSizePerOrder();
                final Simulator.DataInput dataInput = new Simulator.DataInput(k69, N);


                // aSync
                final int row     = i,
                          col     = j;
                Runnable runnable = new Runnable() {

                    public void run() {

                        try {

                            for (int k = 0; k < cellSampleSize; k++) {

                                final K69Data k69Data = simulateK69Data(simulator, dataInput);
                                final Output[] output = runningTimeAnalysis(k69Data, false,
                                                            sizePerOrder,
                                                            state.getScaledSamplerIndex());

                                state.addOutputBySize(output[0], row, col, k);
                                state.addOutputByTime(output[1], row, col, k);
                            }

                            AppNoteIS_CmdState.store(state, statefulPath);

                        } catch (CommandException | InterruptedException | IOException e) {
                            e.printStackTrace();
                        }
                    }
                };

                if (!aSync) runnable.run();
                else {

                    if (threads.size() == jobUnit) {

                        ThreadUtil.waitToFinish(threads);
                        threads.clear();
                    }

                    final Thread thread = new Thread(runnable);

                    threads.add(thread);
                    thread.start();
                }
            }
        }

        if (aSync) {

            ThreadUtil.waitToFinish(threads);
            threads.clear();
        }

        final JPanel gridPanel = AppNoteIS_OC.createGridPanel(state);

        GUIUtil.displayGUI("", gridPanel);
    }

    /**
     * running time comparison for real data sets.
     *
     * @throws CommandException
     * @throws InterruptedException
     */
    void realData_RunningTimeAnalysis() throws CommandException, InterruptedException {

        double[] thetaS        = { 4.8 };
        final K69Data realData = RealDataSets.of_GT94().getValue0();

        for (double theta : thetaS) {

            K69Data k69Data = changeThetaInData(theta, realData);

            runningTimeAnalysis(k69Data, true, 6000, 1);
        }
    }

    /**
     * Runs based on sample size and then based on time. The time is chosen from the first run.
     *
     * @param k69Data
     * @param print
     * @param sizePerOrder
     * @param scaledSamplerIndex
     * @return
     * @throws CommandException
     * @throws InterruptedException
     */
    private Output[] runningTimeAnalysis(K69Data k69Data, boolean print, int sizePerOrder,
            int scaledSamplerIndex)
            throws CommandException, InterruptedException {

        double theta        = k69Data.getModel().getMutationRate();
        final K69_AC k69_ac = new K69_AC(k69Data);

        System.out.println("Theta:              " + format(theta));
        System.out.println("Data Order:         " + format(k69_ac.eventsToMRCA()));
        System.out.println(".... SampleSize:    " + format(k69_ac.getN()));
        System.out.println(".... MutationCount: " + format(k69_ac.getSn()));
        System.out.println(".... AlleleCount:   " + format(k69_ac.getKn()));
        System.out.println();


        // Thread-CPU-Time message
        final boolean threadTimeEnabled = RunSamplers_Cmd.isThreadCpuTimeEnabled();
        String msg                      = "";

        if (threadTimeEnabled) msg = "Using [Thread-CPU-Time] for benchmarking.\n";
        else {
            msg = "[xx-Thread-CPU-Time-xx] is NOT available under the running platform; using [Elapsed-Time] for benchmarking.\n";
        }

        if (print) System.out.println(msg + "\n");

        Output[] output = new Output[2];


        // by sampleSize
        RunSamplers_EvtImpl_Direct runSamplersEvt = new RunSamplers_EvtImpl_Direct(k69_ac,
                                                        sizePerOrder);

        System.out.println("Sampler by SampleSize: ");

        final int maxTimeInSecs = runSamplers(runSamplersEvt, output, 0, print, scaledSamplerIndex)
                                  + 5;


        // by Time
        System.out.println("Sampler by Time: ");

        RunSamplers_EvtImpl_Direct runSamplersEvt1 = new RunSamplers_EvtImpl_Direct(k69_ac,
                                                         sizePerOrder);

        runSamplersEvt1.setIterationByTime(maxTimeInSecs * 1000);
        runSamplers(runSamplersEvt1, output, 1, print, scaledSamplerIndex);

        return output;
    }

    /**
     * Runs multiple samplers and collects output on mean, se, ess and time.
     *
     * @param runSamplersEvt
     * @param output
     * @param outputIndex
     * @param print
     * @param scaledSamplerIndex1
     * @return maximum time taken by any proposal in seconds
     * @throws CommandException
     * @throws InterruptedException
     */
    private int runSamplers(RunSamplers_Evt runSamplersEvt, Output[] output, int outputIndex,
                            boolean print, int scaledSamplerIndex)
            throws CommandException, InterruptedException {

        RunSamplers_Cmd runSamplersCmd = new RunSamplers_Cmd(runSamplersEvt);

        runSamplersCmd.execute(false);


        // output
        String[][] printOutput      = new String[4][6];
        final List<String> samplers = new ArrayList<>(runSamplersCmd.getSamplerNames());

        printOutput[0] = new String[] {
            "", "Prob", "SE", "ESS", "Time(s)", "sampleSize"
        };

        int max_time       = 0;
        Double[] scaledESS = new Double[samplers.size()];
        double[] mean_aRay = new double[samplers.size()];
        double[] se_aRay   = new double[samplers.size()];
        double[] ess_aRay  = new double[samplers.size()];
        int[] size_aRay    = new int[samplers.size()];
        int[] seconds_aRay = new int[samplers.size()];

        for (int i = 0; i < samplers.size(); i++) {

            String sampler        = samplers.get(i);
            final BigDecimal mean = runSamplersCmd.getISMean(sampler);
            final BigDecimal se   = runSamplersCmd.getIS_SE(sampler);
            final BigDecimal ess  = runSamplersCmd.getIS_ESS(sampler);
            final long sampleSize = runSamplersCmd.getSampleSize(sampler);
            final int time        = runSamplersCmd.getElapsedTime(sampler);

            mean_aRay[i]    = mean.doubleValue();
            se_aRay[i]      = se.doubleValue();
            ess_aRay[i]     = ess.doubleValue();
            size_aRay[i]    = (int) sampleSize;
            seconds_aRay[i] = time;

            if (time > max_time) max_time = time;

            printOutput[1 + i] = new String[] {
                sampler, format(mean), format(se), format(ess), format(time), format(sampleSize)
            };
        }

        output[outputIndex] = new Output(mean_aRay, se_aRay, ess_aRay, size_aRay, seconds_aRay);

        if (print) System.out.println(PrintUtil.print2D(printOutput, 2, 1, null));

        return max_time;
    }

    /**
     *  Computes probability by exact and IS method for validation of the latter for accuracy.
     *
     *  Table with columns "Prob", "SE", "ESS", "Time(s)" and rows corresponding to the exact and
     *  the sampler proposals. this repeated for multiple theta (1.0, 2.0, 3.0) and fixed N = 25.
     *
     * @param dataSimulator a data simulator
     * @param sizePerOrder
     * @throws CommandException
     * @throws InterruptedException
     */
    void runValidationWithExact(Simulator.DATA<K69, K69Data> dataSimulator, int sizePerOrder)
            throws CommandException, InterruptedException {

        double[] exactTheta = { 1.0, 2.0, 3.0 };
        int exactN          = 25;

        for (double theta : exactTheta) {

            K69 k69                             = new K69(theta);
            final Simulator.DataInput dataInput = new Simulator.DataInput(k69, exactN);
            final K69Data k69Data = dataSimulator.simulate(dataInput, 1).iterator().next();

            if (k69Data == null) {

                System.out.println("Skipping... data generated w/ site count 0");

                continue;
            }

            final K69_AC k69_ac = new K69_AC(k69Data);


            // exact
            final ProbComputer_Exact probComputer = RecursionComputers.getExactProbComputer();
            RunRecursionEvent.DEFAULT exactEvt = new RunRecursionEventImpl_Direct.DEFAULT(k69,
                                                     k69_ac, probComputer);
            RunRecursionCommand.DEFAULT exactCmd  = new RunRecursionCommand.DEFAULT(exactEvt);

            exactCmd.execute(false);

            final BigDecimal exactProb = probComputer.getResult();


            // samplers todo reuse runSamplers
            RunSamplers_Evt runSamplersEvt = new RunSamplers_EvtImpl_Direct(k69_ac, sizePerOrder);
            RunSamplers_Cmd runSamplersCmd = new RunSamplers_Cmd(runSamplersEvt);

            runSamplersCmd.execute(false);


            // output
            System.out.println("Theta: " + format(theta));

            String[][] output           = new String[5][5];
            final List<String> samplers = new ArrayList<>(runSamplersCmd.getSamplerNames());

            output[0] = new String[] { "", "Prob", "SE", "ESS", "Time(s)" };
            output[1] = new String[] { "Exact", format(exactProb), "-", "-", "-" };

            for (int i = 0; i < samplers.size(); i++) {

                String sampler        = samplers.get(i);
                final BigDecimal mean = runSamplersCmd.getISMean(sampler);
                final BigDecimal se   = runSamplersCmd.getIS_SE(sampler);
                final BigDecimal ess  = runSamplersCmd.getIS_ESS(sampler);
                final int time        = runSamplersCmd.getElapsedTime(sampler);

                output[2 + i] = new String[] { sampler, format(mean), format(se), format(ess),
                                               format(time) };
            }

            System.out.println(PrintUtil.print2D(output, 2, 1, null));
        }
    }

    /**
     *
     */
    private static class RunSamplers_EvtImpl_Direct extends EvtImpl_Direct
            implements RunSamplers_Evt {

        private final Sampler<?> egt, sd, huw;

        private RunSamplers_EvtImpl_Direct(K69_AC ac, int sizePerOrderUnit) {

            egt = Samplers_K69.of_GT_EGT(ac, sizePerOrderUnit);
            sd  = Samplers_K69.of_GT_SD(ac, sizePerOrderUnit);
            huw = Samplers_K69.of_GT_HUW(ac, sizePerOrderUnit);
        }

        public void setIterationByTime(int millis) {

            egt.setIteratorByTime(millis);
            sd.setIteratorByTime(millis);
            huw.setIteratorByTime(millis);
        }

        @Override
        public List<Sampler<?>> getSamplers() {
            return Arrays.asList(egt, sd, huw);
        }

        /**
         * @return labels to be used in the plot in the same order as the sampler objects;
         * need not be the canonical names
         */
        private static String[] samplerLabels() {
            return new String[] { "EGT", "SD", "HUW" };
        }

        @Override
        public GUI getGUISettings() {
            return null;
        }
    }

    public AppNoteIS_CmdState getState() {
        return null;
    }

    private K69Data simulateK69Data(final Simulator.DATA<K69, K69Data> simulator,
                                    final Simulator.DataInput dataInput) {

        for (int i = 0; i < 4; i++) {

            final K69Data k69Data = simulator.simulate(dataInput, 1).iterator().next();

            if (k69Data != null) return k69Data;
        }

        throw new RuntimeException(
            "Data simulation problem: too many data sets simulated with zero sites");
    }

    /**
     * change the theta in the data
     *
     * @param theta
     * @param data
     * @return
     */
    private K69Data changeThetaInData(double theta, K69Data data) {

        final MutableData<K69, K69Data> mutable = data.getMutable();

        mutable.setModel(new K69(theta));

        return mutable.getImmutable();
    }

    private boolean isStatePresent() {
        return new File(statefulPath).exists();
    }
}
